package cc.eddic.practice.concurrency.impl;

import cc.eddic.practice.concurrency.LogItem;
import cc.eddic.practice.concurrency.LogItemParser;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;

@Slf4j
@Component
@Qualifier("LogItemParserImpl")
public class LogItemParserImpl implements LogItemParser {
    @Override
    public Optional<LogItem> parse(String line) {
        val lineMatcher = LOG_PATTERN.matcher(line);
        if (!lineMatcher.matches()) {
//            log.error("Error parsing line: {}", line);
            if (log.isDebugEnabled()) log.debug("Error parsing line: {}", line);
            return Optional.empty();
        }

        LocalDateTime ldt;
        try {
            ldt = LocalDateTime.parse(lineMatcher.group("dt"), DATE_TIME_FORMATTER);
        } catch (DateTimeParseException ex) {
            log.error("Error parsing time: {}", lineMatcher.group("dt"), ex);
            return Optional.empty();
        }

        int status;
        try {
            status = Integer.parseInt(lineMatcher.group("status"));
        } catch (NumberFormatException ex) {
            log.error("Error parsing status: {}", lineMatcher.group("status"), ex);
            return Optional.empty();
        }

        long bytes;
        try {
            bytes = Long.parseLong(lineMatcher.group("sent"));
        } catch (NumberFormatException ex) {
            log.error("Error parsing bytes: {}", lineMatcher.group("sent"), ex);
            return Optional.empty();
        }

        val req = request(lineMatcher.group("request"), line);
        return Optional.of(new LogItem(lineMatcher.group("ip"), ldt, lineMatcher.group("request"), req.getMethod(), req.getUrl(), req.getQuery(), req.getProtocol(), status, bytes, lineMatcher.group("referer"), lineMatcher.group("ua")));
    }

    protected RequestModel request(String request, String line) {
        String method = null, protocol = null, path = null;
        Map<String, String> query = new HashMap<>();
        val requestMatcher = REQUEST_PATTERN.matcher(request);
        if (requestMatcher.matches()) {
            method = requestMatcher.group("method");
            protocol = requestMatcher.group("protocol");

            val o = parseUrlModel(requestMatcher.group("url"));
            if (o.isPresent()) {
                path = o.get().getUrl();
                query = o.get().getQuery();
            } else {
                path = requestMatcher.group("url");
            }
        } else {
            if (log.isTraceEnabled()) log.trace("Error parsing request: {}\n{}", request, line);
        }
        return new RequestModel(method, path, query, protocol);
    }

    private String decode(String url) {
        try {
            return URLDecoder.decode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            log.error(e.getMessage(), e);
            return url;
        }
    }

    private Optional<UrlModel> parseUrlModel(String url) {
        val x = url.split("\\?");
        if (x.length < 1) {
            if (log.isTraceEnabled()) log.trace("Error parsing url: {}", url);
            return Optional.empty();
        }

        Map<String, String> query = new HashMap<>();
        if (x.length > 1) {
            val q = x[1].split("&");
            for (String s : q) {
                val kv = s.split("=");
                if (kv.length == 2) {
                    query.put(kv[0], decode(kv[1]));
                }
            }
            if (log.isTraceEnabled()) log.trace("query {} of {}", query.size(), q);
        }
        return Optional.of(new UrlModel(x[0], query));
    }

    private static final Pattern LOG_PATTERN = Pattern.compile("(?<ip>\\d+\\.\\d+\\.\\d+\\.\\d+)\\s+[\\w\\-]+\\s+[\\w\\-]+\\s+\\[(?<dt>\\d+/\\w+/\\d+:\\d+:\\d+:\\d+\\s+\\+\\d+)]\\s+\"(?<request>.*)\"\\s+(?<status>\\d+)\\s+(?<sent>\\d+)\\s+\"(?<referer>[^\"]+)\"\\s+\"(?<ua>[^\"]+)\"");

    private static final Pattern REQUEST_PATTERN = Pattern.compile("(?<method>\\w+)\\s+(?<url>[^ ]+)\\s+(?<protocol>[^ ]+)");

    protected static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MMM/yyyy:HH:mm:ss Z", Locale.ENGLISH);

    @Getter
    @AllArgsConstructor
    private static class UrlModel {
        private String url;
        private Map<String, String> query;
    }

    @Getter
    @AllArgsConstructor
    protected static class RequestModel {
        private String method;
        private String url;
        private Map<String, String> query;
        private String protocol;
    }
}
